射频识别(RFID)技术

实验:数据块读写操作

作者:陈广 日期:2019-2-21


实验目的

Mifare S50 卡中的数据块可用于存放数据及值,两者的格式不同,本实验讲解数据的读写操作。我们知道,一个数据块最多只能存放 16 个字节的数据,如果存放的数据超过 16 个字节,就需要进行多次读写,控制上相对比较麻烦。

简单读写

我们首先掌握最简单的读写操作,就是读取或写入整个数据块 16 个字节的内容。在进行读写操作前,需要进行请求、防冲突、选卡、证实等一系列操作,这些操作前面都已经介绍,这次我们将其包装在一个方法内。方便在多个程序中调用。

读数据

一张新的电子标签里的数据块内容都会被初始化为 0,但扇区 0 用于存放标签 UID 及厂商代码,我们可以尝试读取这部分内容。

新建一个控制台应用程序,输入如下代码:

using System;
using System.Threading.Tasks;
using System.Text;
using HBLib;
using HBLib.HR8002Reader;
using HBLib.ISO14443A;

namespace CmdDemo
{
    class Program
    {
        static ComPort com = new ComPort("COM3", 19200, 300);
        static Reader reader;
        static I14443A i14443a;

        static void Main(string[] args)
        {
            com.Open();
            Read(0);

            Console.ReadLine();
        }

        //读取扇区 0 的第 0 块中的厂商代码
        private static async Task Read(byte blockNum)
        {
            if (await AuthKey())
            {
                var info = await i14443a.ReadAsync(blockNum);
                if (info.ReturnValue == ReturnMessage.Success)
                {
                    Console.WriteLine("读取的数据:" + info.GetBlockDataStr());
                }
                else
                {
                    Console.WriteLine(info.GetStatusStr());
                }
            }
        }

        //从请求到证实等一系列操作
        private static async Task<bool> AuthKey()
        {
            reader = new Reader(0x00, com);
            i14443a = new I14443A(0x00, com);

            await reader.ChangeToISO14443AAsync();
            //请求操作
            var info = await i14443a.RequestAsync(RequestMode.AllCard);
            if (info.ReturnValue != ReturnMessage.Success)
            {
                Console.WriteLine(info.GetStatusStr());
                return false;
            }
            //防冲突操作
            var info1 = await i14443a.AnticollAsync();
            if (info1.ReturnValue != ReturnMessage.Success)
            {
                Console.WriteLine(info1.GetStatusStr());
                return false;
            }
            //选卡操作
            var info2 = await i14443a.SelectAsync(info1.UID);
            if (info2.ReturnValue == ReturnMessage.Success)
            {
                Console.WriteLine("选中标签:" + info1.GetUIDStr());
            }
            else
            {
                Console.WriteLine(info2.GetStatusStr());
                return false;
            }

            byte[] keyA = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
            //密码 A 证实
            var info3 = await i14443a.AuthKeyAsync(KeyType.KeyA, 0, keyA);
            if (info3.ReturnValue != ReturnMessage.Success)
            {
                Console.WriteLine(info3.GetStatusStr());
                return false;
            }
            return true;
        }
    }
}

运行程序,结果如下:

选中标签:A98E03BF
读取的数据:A9 8E 03 BF 9B 08 04 00 01 96 CF B9 26 B6 2B 1D

可以看到块 0 中的前四个字节存储的是标签的 UID。此程序假定标签的第一扇区密码 A 为默认值。

写数据

接下来我们在程序中加入写数据的代码,向块 1 写入数据,16字节数据依次为 1 到 16。下面只列出更改的代码,其余同上。

static void Main(string[] args)
{
    com.Open();
    Write(1);

    Console.ReadLine();
}

//读取扇区 0 的第 0 块中的厂商代码
private static async Task Read(byte blockNum)
{
    var info = await i14443a.ReadAsync(blockNum);
    if (info.ReturnValue == ReturnMessage.Success)
    {
        Console.WriteLine("读取的数据:" + info.GetBlockDataStr());
    }
    else
    {
        Console.WriteLine(info.GetStatusStr());
    }
}
//写入数据
private static async Task Write(byte blockNum)
{
    if (await AuthKey())
    {
        byte[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
        var info = await i14443a.WriteAsync(blockNum, data);
        if (info.ReturnValue == ReturnMessage.Success)
        {
            await Read(blockNum);
        }
        else
        {
            Console.WriteLine(info.GetStatusStr());
        }
    }
}

运行程序,结果如下:

选中标签:A98E03BF
读取的数据:01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10

我们看到,数据已经写入标签,在写入数据后,立即调用Read方法将其读出并打印。

存取字符串

下面来个复杂点的场景。假设有一张 VIP 卡,需要存储用户姓名、电话、地址。我们设计将 0 号扇区的 1 号块存储姓名,2 号块存储电话,4 号扇区的 3 个数据块存储地址。

需要解决两个问题:

  1. 一个数据块 16 个字节,姓名无法全部,如何知道姓名占用的字节长度呢?
  2. 地址肯定超过 16 个字节,需要跨数据块存储,如何判断地址使用了几个数据块呢?

解决上述问题可以有两种方案:

  1. 字符串的第一个字节用来存储字符串所占字节长度。
  2. 遇到 0 字节表明字符串结束

第 1 种方案在算法上更容易实现;第 2 种方案可以节省 3 个字节的存储空间,但算法实现上太麻烦,比如地址正好占用两个数据块 32 个字节,但你还是要读第三个数据块方能判断是否字符串已经结束。如果给定空间正好用完,又无法在最后加 0 等等情况,总之做起来很麻烦。所以最终决定使用第一种方案,这也是网络协议最常用的方案。

写入字符串

只有将数据先写入标签,才有数据可读,所以我们先讲写操作。原本是写0、1号扇区的,结果没注意,地址超出 3 个数据块的存储空间,数据写到了控制块内,杯具了,坏了 3 个扇区才发现是数据超出长度的原因,所以最后程序变成写0、4号扇区。血的教训啊!大家千万要小心!代码如下:

static ComPort com = new ComPort("COM3", 19200, 300);
static Reader reader;
static I14443A i14443a;

static void Main(string[] args)
{
    com.Open();
    WriteInfo();

    Console.ReadLine();
}

private static async Task WriteInfo()
{
    string name = "张三丰";
    string phone = "0771-3246041";
    string addr = "广西南宁市大学路,广西机电职业技术学院";

    //选卡
    if(!await Select())
    {
        Console.WriteLine("选卡失败!");
        return;
    }
    //证实 0 扇区
    if(!await AuthKey(0))
    {
        Console.WriteLine("证实 0 扇区失败!");
        return;
    }
    //写入姓名
    byte[] bytes = Encoding.Unicode.GetBytes(name);
    byte[] nameBytes = new byte[bytes.Length + 1];
    nameBytes[0] = (byte)bytes.Length;
    bytes.CopyTo(nameBytes, 1);
    if (!await Write(1, nameBytes))
    {
        Console.WriteLine("写入姓名失败!");
        return;
    }
    //写入电话
    bytes = Encoding.ASCII.GetBytes(phone);
    byte[] phoneBytes = new byte[bytes.Length + 1];
    phoneBytes[0] = (byte)bytes.Length;
    bytes.CopyTo(phoneBytes, 1);
    if (!await Write(2, phoneBytes))
    {
        Console.WriteLine("写入电话失败!");
        return;
    }

    //证实 4 号扇区
    if (!await AuthKey(4))
    {
        Console.WriteLine("证实 4 扇区失败!");
        return;
    }
    //写入地址
    bytes = Encoding.Unicode.GetBytes(addr);
    int len = bytes.Length + 1;
    if(len>48)
    {
        Console.WriteLine("地址长度不能超过 48 个字节,请缩短地址!");
        return;
    }
    byte[] addrByte = new byte[len];
    addrByte[0] = (byte)bytes.Length;
    bytes.CopyTo(addrByte, 1);
    //将地址分为16字节一组写入多个数据块
    for (int i = 0; i < len; i += 16) 
    {
        int num = ((len - i) < 16) ? len - i : 16;
        byte[] buff = new byte[num];
        Array.Copy(addrByte, i, buff, 0, num);
        if (!await Write((byte)(i / 16 + 16), buff))
        {
            Console.WriteLine("写入地址失败!");
            return;
        }
    }
    Console.WriteLine("成功写入数据!");
}
//向指定扇区写入指定数据
private static async Task<bool> Write(byte blockNum, byte[] data)
{
    byte sectorNum = (byte)(blockNum / 4);
    if (await AuthKey(sectorNum))
    {
        var info = await i14443a.WriteAsync(blockNum, data);
        if (info.ReturnValue == ReturnMessage.Success)
        {
            return true;
        }
        else
        {
            Console.WriteLine(info.GetStatusStr());
            return false;
        }
    }
    return false;
}
//选卡
private static async Task<bool> Select()
{
    reader = new Reader(0x00, com);
    i14443a = new I14443A(0x00, com);

    await reader.ChangeToISO14443AAsync();
    //请求操作
    var info = await i14443a.RequestAsync(RequestMode.AllCard);
    if (info.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info.GetStatusStr());
        return false;
    }
    //防冲突操作
    var info1 = await i14443a.AnticollAsync();
    if (info1.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info1.GetStatusStr());
        return false;
    }
    //选卡操作
    var info2 = await i14443a.SelectAsync(info1.UID);
    if (info2.ReturnValue == ReturnMessage.Success)
    {
        Console.WriteLine("选中标签:" + info1.GetUIDStr());
    }
    else
    {
        Console.WriteLine(info2.GetStatusStr());
        return false;
    }
    return true;
}
//证实指定扇区
private static async Task<bool> AuthKey(byte sectorNum)
{
    byte[] keyA = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
    //密码 A 证实
    var info3 = await i14443a.AuthKeyAsync(KeyType.KeyA, sectorNum, keyA);
    if (info3.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info3.GetStatusStr());
        return false;
    }
    return true;
}

运行程序,成功写入数据后,可以打开厂家自带的上位机 Demo 查看扇区 0 和扇区 4 的相应数据块的数据,以证实您确实将数据写入到相应的地方了。

读取字符串

VIP 信息已经写入标签内,接下来我们尝试将其读出。将代码更改如下:

static ComPort com = new ComPort("COM3", 19200, 300);
static Reader reader;
static I14443A i14443a;

static void Main(string[] args)
{
    com.Open();
    ReadInfo();

    Console.ReadLine();
}

private static async Task ReadInfo()
{
    //选卡
    if (!await Select())
    {
        Console.WriteLine("选卡失败!");
        return;
    }
    //证实 0 扇区
    if (!await AuthKey(0))
    {
        Console.WriteLine("证实 0 扇区失败!");
        return;
    }
    //读取姓名
    byte[] buff = await Read(1);
    if (buff != null)
    {
        byte[] nameByte = new byte[buff[0]];
        Array.Copy(buff, 1, nameByte, 0, buff[0]);
        string name = Encoding.Unicode.GetString(nameByte);
        Console.WriteLine("姓名:" + name);
    }
    else
    {
        Console.WriteLine("读取姓名失败!");
    }
    //读取电话
    buff = await Read(2);
    if (buff != null)
    {
        byte[] phoneByte = new byte[buff[0]];
        Array.Copy(buff, 1, phoneByte, 0, buff[0]);
        string phone = Encoding.ASCII.GetString(phoneByte);
        Console.WriteLine("电话:" + phone);
    }
    else
    {
        Console.WriteLine("读取姓名失败!");
    }

    //证实 4 号扇区
    if (!await AuthKey(4))
    {
        Console.WriteLine("证实 4 扇区失败!");
        return;
    }
    //读取地址
    byte blockNum = 16;
    buff = await Read(blockNum);
    if (buff != null)
    {
        int len = buff[0];
        byte[] addrByte = new byte[len]; //存放地址用的字节数组
        int index = 0;
        int copyLen = len < 15 ? len : 15; //每次要拷贝的数组长度
        Array.Copy(buff, 1, addrByte, 0, copyLen);
        while ((index += copyLen) < len)
        {
            blockNum++;
            buff = await Read(blockNum);
            copyLen = len - index < 16 ? len - index : 16;
            Array.Copy(buff, 0, addrByte, index, copyLen);
        }
        string addr = Encoding.Unicode.GetString(addrByte);
        Console.WriteLine("地址:" + addr);
    }
    else
    {
        Console.WriteLine("读取地址失败!");
        return;
    }
}
//读取指定扇区数据
private static async Task<byte[]> Read(byte blockNum)
{
    byte sectorNum = (byte)(blockNum / 4);
    if (await AuthKey(sectorNum))
    {
        var info = await i14443a.ReadAsync(blockNum);
        if (info.ReturnValue == ReturnMessage.Success)
        {
            return info.BlockData;
        }
        else
        {
            Console.WriteLine(info.GetStatusStr());
            return null;
        }
    }
    return null;
}
//选卡
private static async Task<bool> Select()
{
    reader = new Reader(0x00, com);
    i14443a = new I14443A(0x00, com);

    await reader.ChangeToISO14443AAsync();
    //请求操作
    var info = await i14443a.RequestAsync(RequestMode.AllCard);
    if (info.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info.GetStatusStr());
        return false;
    }
    //防冲突操作
    var info1 = await i14443a.AnticollAsync();
    if (info1.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info1.GetStatusStr());
        return false;
    }
    //选卡操作
    var info2 = await i14443a.SelectAsync(info1.UID);
    if (info2.ReturnValue == ReturnMessage.Success)
    {
        Console.WriteLine("选中标签:" + info1.GetUIDStr());
    }
    else
    {
        Console.WriteLine(info2.GetStatusStr());
        return false;
    }
    return true;
}
//证实指定扇区
private static async Task<bool> AuthKey(byte sectorNum)
{
    byte[] keyA = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
    //密码 A 证实
    var info3 = await i14443a.AuthKeyAsync(KeyType.KeyA, sectorNum, keyA);
    if (info3.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info3.GetStatusStr());
        return false;
    }
    return true;
}

运行程序,结果如下:

选中标签:A98E03BF
姓名:张三丰
电话:0771-3246041
地址:广西南宁市大学路,广西机电职业技术学院

写完这个代码后,发现使用将字符串长度放在第一个字节这种方案的算法也不是那么好写,因为要读第一个数据块才能取出整个字符串的长度。这使得第一个数据块和其它数据块的读取要分开来写。

存取复杂对象

上面存储 VIP 用户信息的方式总感觉不是太靠谱,写起代码来非常麻烦,惟一好处是可以单独读写某个信息而不影响其它的信息。

更好的方法当然是使用序列化,我们将信息包装成对象,然后将对象序列化之后进行存储到标签之中。需要用的时候从标签读出数据,然后反序列化就可以使用了。有两个途径进行序列化,第一是使用 C# 自带的序列化功能将对象转化为字节流;第二是使用 Newtonsoft NuGet 包将对象序列化为 JSON 字符串,再转化为字节数组进行存储。

两种方法我都试过了,第一种方法得到的字节数组占用空间最大,这点是我没想到的。第二种方法中使用 UTF8 编码占用空间最小。最终我决定使用强大的 NewtonSoft 来实现序列化功能。JSON 建议大家去学学,肯定会用到的。而 NewtonSoft 就是一个强大的 JSON 工具,.NET Core 已经自带 NewtonSoft 开发包。.NET Framework 需要引入 NuGet 包。但有消息说 .NET Core 3.0 后微软会使用自己开发的 JSON 包,不再自带 NewtonSoft。

将复杂对象存进电子标签

新建一个控制台应用程序,在 Visual Studio 菜单中选择【工具】➤【NuGet 包管理器】➤【管理解决方案的 NuGet 程序包】,选中【浏览】选项卡,在搜索栏中输入“NewtonSoft”。此时搜索出【Newtonsoft.Json】,单击它,在右边窗口中勾选你刚创建的项目,单击下方【安装】按钮安装 NuGet 包。如下图所示:

图 1:安装 NewtonSoft

接下来我们新建一个 VIP 信息类:

class VipInfo
{
    public string Name { get; set; } = "张三丰";
    public bool Sex { get; set; } = true; //true为男,false为女
    public DateTime Birthday { get; set; } = DateTime.Parse("1990-01-01");
    public string Phone { get; set; } = "0771-3246041";
    public string Url { get; set; } = "www.iotxfd.cn";
    public string Address { get; set; } = "广西南宁市大学路,广西机电职业技术学院";
}

这一次,我们使用了更为复杂的信息,不单有字符串,还有布尔值、日期值。整个对象序列化后,需使用 179 个字节。需使用 4 个扇区方能存储完毕。接下来我们编写程序,实现跨扇区存储数据,而且根据信息长度自动决定跨区数量。写标签代码如下:

static ComPort com = new ComPort("COM3", 19200, 300);
static Reader reader;
static I14443A i14443a;

static void Main(string[] args)
{
    com.Open();
    reader = new Reader(0x00, com);
    i14443a = new I14443A(0x00, com);

    VipInfo vip = new VipInfo();
    string str = JsonConvert.SerializeObject(vip); //将对象序列化为字符串
    byte[] vipByte = Encoding.UTF8.GetBytes(str); //将字符串转化为字节数组
    WriteInfo(6, vipByte); //从第 6 个扇区开始写入

    Console.ReadLine();
}

/// <summary>
/// 将 data 写入 sectorNum 开始的扇区,自动跨区存储
/// </summary>
/// <param name="sectorNum">从此扇区开始存储</param>
/// <param name="data">写入的数据</param>
/// <returns></returns>
private static async Task WriteInfo(byte sectorNum, byte[] data)
{
    if (sectorNum == 0)
    {
        Console.WriteLine("不能从 0 扇区开始写入!");
        return;
    }
    if (!await Select()) return;
    //先在字节数组前面压入长度,使用两个字节存储长度
    byte[] buff = new byte[data.Length + 2];
    byte[] lenByte = BitConverter.GetBytes((UInt16)data.Length);
    lenByte.CopyTo(buff, 0); //压入数据长度
    data.CopyTo(buff, 2); //压入数据

    int blockNum = sectorNum * 4; //初始绝对块号
    int len = buff.Length; //压入的数据长度
    int index = 0; //存储进度
    while (index < len)
    {
        if ((index / 16) % 3 == 0) //此时存储地址每个扇区的第一个数据块
        {
            if (!await AuthKey(sectorNum)) return;
            sectorNum++;
        }
        int copyLen = (len - index < 16) ? len - index : 16; //每次要拷贝的数组的长度
        byte[] wByte = new byte[copyLen];
        Array.Copy(buff, index, wByte, 0, copyLen);
        if (!await Write((byte)blockNum, wByte)) return;
        blockNum = ((index / 16) % 3 == 2) ? blockNum + 2 : blockNum + 1;
        index += copyLen;
    }
    Console.WriteLine("VIP 信息已经写入标签!");
}

//向指定扇区写入指定数据
private static async Task<bool> Write(byte blockNum, byte[] data)
{
    var info = await i14443a.WriteAsync(blockNum, data);
    if (info.ReturnValue == ReturnMessage.Success)
    {
        return true;
    }
    else
    {
        Console.WriteLine(info.GetStatusStr());
        return false;
    }
}

//选卡
private static async Task<bool> Select()
{
    reader = new Reader(0x00, com);
    i14443a = new I14443A(0x00, com);

    await reader.ChangeToISO14443AAsync();
    //请求操作
    var info = await i14443a.RequestAsync(RequestMode.AllCard);
    if (info.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info.GetStatusStr());
        return false;
    }
    //防冲突操作
    var info1 = await i14443a.AnticollAsync();
    if (info1.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info1.GetStatusStr());
        return false;
    }
    //选卡操作
    var info2 = await i14443a.SelectAsync(info1.UID);
    if (info2.ReturnValue == ReturnMessage.Success)
    {
        Console.WriteLine("选中标签:" + info1.GetUIDStr());
    }
    else
    {
        Console.WriteLine(info2.GetStatusStr());
        return false;
    }
    return true;
}
//证实指定扇区
private static async Task<bool> AuthKey(byte sectorNum)
{
    byte[] keyA = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
    //密码 A 证实
    var info3 = await i14443a.AuthKeyAsync(KeyType.KeyA, sectorNum, keyA);
    if (info3.ReturnValue != ReturnMessage.Success)
    {
        Console.WriteLine(info3.GetStatusStr());
        return false;
    }
    return true;
}

写标签的主要逻辑全部在WriteInfo方法内,其它都是之前讲过的代码,这一次清爽多了,一次写入多个扇区,并自动避开控制块。运行结果如下:

选中标签:A98E03BF
VIP 信息已经写入标签!

很不幸,又写废了两个扇区,所以从第 6 号扇区开始写入。有模拟器就不会出这样的问题了,但模拟器得等到开学了才能继续完成。

需要注意的是:

  1. 由于一个字节最大数字是 255,而压入字节的长度有可能超过这个数字,所以这一次,我们使用两个字节来存储字节数组长度。
  2. 选择过于靠后的扇区可能导致存储区域不足,为简化程序,这里没有做任何判断。需自行计算。
  3. 不能从 0 扇区开始写入,因为 0 号块用于存储厂商信息,不可更改。当然,加多点代码也可以从 0 扇区的 1 号块开始写入。

思考题(难度 **):更改程序,使程序具备存储区域是否不足的功能,在空间不足的情况下给出用户提示,并退出写卡操作。另外再增加从 0 扇区开始写入的功能。使得WriteInfo(byte sectorNum, byte[] data)方法的sectorNum参数可以为 0。

读取复杂对象

标签里已经有了数据,接下来,我们把它读出来。

在代码中添加ReadInfo方法:

private static async Task ReadInfo(byte sectorNum)
{
    if (sectorNum == 0)
    {
        Console.WriteLine("不能从 0 扇区开始读取!");
        return;
    }
    if (!await Select()) return; //选卡
    int blockNum = sectorNum * 4;
    int index = 0, len = 1;
    int copyLen = 14; //每次要拷贝的数组长度
    byte[] vipByte = null;
    while (index < len)
    {
        if (blockNum % 4 == 0) //证实
        {
            if (!await AuthKey((byte)(blockNum / 4)))
            {
                return;
            }
        }
        var info = await i14443a.ReadAsync((byte)blockNum);
        if (info.ReturnValue != ReturnMessage.Success)
        {
            Console.WriteLine("块" + blockNum + "读写错误:" + info.GetStatusStr());
            return;
        }
        byte[] buff = info.BlockData;
        if (index == 0) //第一个块包含数据长度,需单独处理
        {
            len = BitConverter.ToUInt16(buff, 0);
            vipByte = new byte[len];
            copyLen = len < 14 ? len : 14;
            Array.Copy(buff, 2, vipByte, index, copyLen);
        }
        else //处理除第一个块的其它块
        {
            copyLen = len - index < 16 ? len - index : 16;
            Array.Copy(buff, 0, vipByte, index, copyLen);
        }
        index += copyLen;
        blockNum = (blockNum % 4 == 2) ? blockNum + 2 : blockNum + 1;
    }

    //打印出 VIP 信息
    string str = Encoding.UTF8.GetString(vipByte);
    VipInfo vip = JsonConvert.DeserializeObject<VipInfo>(str);
    Console.WriteLine("姓名:" + vip.Name);
    Console.WriteLine("性别:" + (vip.Sex ? "男" : "女"));
    Console.WriteLine("出生年月:" + vip.Birthday.ToShortDateString());
    Console.WriteLine("电话号码:" + vip.Phone);
    Console.WriteLine("网址:" + vip.Url);
    Console.WriteLine("地址:" + vip.Address);
}

更改Main方法如下:

static void Main(string[] args)
{
    com.Open();
    reader = new Reader(0x00, com);
    i14443a = new I14443A(0x00, com);

    ReadInfo(6); //从第 6 个扇区开始读信息

    Console.ReadLine();
}

运行程序,结果如下:

选中标签:A98E03BF
姓名:张三丰
性别:男
出生年月:1990/1/1
电话号码:0771-3246041
网址:www.iotxfd.cn
地址:广西南宁市大学路,广西机电职业技术学院

从结果得知,我们刚才写进去的数据已经完全正确读出。

思考题(难度 **):增加从 0 扇区开始读取的功能。使得ReadInfo(byte sectorNum)方法的sectorNum参数可以为 0。

;

© 2018 - IOT小分队文章发布系统 v0.3